
package uk.co.alt236.bluetoothlelib.device;

import ohos.bluetooth.ble.BleScanResult;
import ohos.utils.Parcel;
import ohos.utils.Sequenceable;
import uk.co.alt236.bluetoothlelib.device.adrecord.AdRecordStore;
import uk.co.alt236.bluetoothlelib.util.AdRecordUtils;
import uk.co.alt236.bluetoothlelib.util.ByteUtils;
import uk.co.alt236.bluetoothlelib.util.LimitedLinkHashMap;

import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * This is a wrapper around the default BleScanResult  object
 * As BleScanResult  is final it cannot be extended, so to get it you
 * need to call {@link #getDevice()} method.
 *
 * @author Alexandros Schillings
 */
public class BluetoothLeDevice implements Sequenceable {

    //反序列化功能
    public static final Producer<BluetoothLeDevice> PRODUCER = new Producer<BluetoothLeDevice>() {

        public BluetoothLeDevice createFromParcel(Parcel in) {
            // Initialize an instance first, then do customized unmarshlling.
            BluetoothLeDevice adrecordstore = new BluetoothLeDevice();
            adrecordstore.unmarshalling(in);
            return adrecordstore;
        }

        public BluetoothLeDevice[] newArray(final int size) {
            return new BluetoothLeDevice[size];
        }
    };

    protected static final int MAX_RSSI_LOG_SIZE = 10;
    private static final String PARCEL_EXTRA_BLUETOOTH_DEVICE = "bluetooth_device";
    private static final String PARCEL_EXTRA_CURRENT_RSSI = "current_rssi";
    private static final String PARCEL_EXTRA_CURRENT_TIMESTAMP = "current_timestamp";
    private static final String PARCEL_EXTRA_DEVICE_RSSI_LOG = "device_rssi_log";
    private static final String PARCEL_EXTRA_DEVICE_SCANRECORD = "device_scanrecord";
    private static final String PARCEL_EXTRA_DEVICE_SCANRECORD_STORE = "device_scanrecord_store";
    private static final String PARCEL_EXTRA_FIRST_RSSI = "device_first_rssi";
    private static final String PARCEL_EXTRA_FIRST_TIMESTAMP = "first_timestamp";
    private static final long LOG_INVALIDATION_THRESHOLD = 10 * 1000;
    private AdRecordStore mRecordStore;
    private BleScanResult mDevice;
    private Map<Long, Integer> mRssiLog;
    private byte[] mScanRecord;
    private int mFirstRssi;
    private long mFirstTimestamp;
    private int mCurrentRssi;
    private long mCurrentTimestamp;

    /**
     * Instantiates a new Bluetooth LE device.
     *
     * @param device     a standard Bluetooth device
     * @param rssi       the RSSI value of the Bluetooth device
     * @param scanRecord the scan record of the device
     * @param timestamp  the timestamp of the RSSI reading
     */
    public BluetoothLeDevice(final BleScanResult device, final int rssi, final byte[] scanRecord, final long timestamp) {
        mDevice = device;
        mFirstRssi = rssi;
        mFirstTimestamp = timestamp;
        mRecordStore = new AdRecordStore(AdRecordUtils.parseScanRecordAsSparseArray(scanRecord));
        this.mScanRecord = scanRecord.clone();
        mRssiLog = new LimitedLinkHashMap<>(MAX_RSSI_LOG_SIZE);
        updateRssiReading(timestamp, rssi);
    }

    /**
     * Instantiates a new Bluetooth LE device
     *
     * @param device the device
     */
    public BluetoothLeDevice(final BluetoothLeDevice device) {
        mCurrentRssi = device.getRssi();
        mCurrentTimestamp = device.getTimestamp();
        mDevice = device.getDevice();
        mFirstRssi = device.getFirstRssi();
        mFirstTimestamp = device.getFirstTimestamp();
        mRecordStore = new AdRecordStore(
                AdRecordUtils.parseScanRecordAsSparseArray(device.getScanRecord()));
        mRssiLog = device.getRssiLog();
        mScanRecord = device.getScanRecord();
    }

    public BluetoothLeDevice() {

    }

    /**
     * Adds the to rssi log.
     *
     * @param timestamp   the timestamp
     * @param rssiReading the rssi reading
     */
    private void addToRssiLog(final long timestamp, final int rssiReading) {
        synchronized (mRssiLog) {
            if (timestamp - mCurrentTimestamp > LOG_INVALIDATION_THRESHOLD) {
                mRssiLog.clear();
            }

            mCurrentRssi = rssiReading;
            mCurrentTimestamp = timestamp;
            mRssiLog.put(timestamp, rssiReading);
        }
    }

    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final BluetoothLeDevice other = (BluetoothLeDevice) obj;
        if (mCurrentRssi != other.mCurrentRssi)
            return false;
        if (mCurrentTimestamp != other.mCurrentTimestamp)
            return false;
        if (mDevice == null) {
            if (other.mDevice != null)
                return false;
        } else if (!mDevice.equals(other.mDevice))
            return false;
        if (mFirstRssi != other.mFirstRssi)
            return false;
        if (mFirstTimestamp != other.mFirstTimestamp)
            return false;
        if (mRecordStore == null) {
            if (other.mRecordStore != null)
                return false;
        } else if (!mRecordStore.equals(other.mRecordStore))
            return false;
        if (mRssiLog == null) {
            if (other.mRssiLog != null)
                return false;
        } else if (!mRssiLog.equals(other.mRssiLog))
            return false;
        if (!Arrays.equals(mScanRecord, other.mScanRecord))
            return false;
        return true;
    }

    /**
     * Gets the ad record store.
     *
     * @return the ad record store
     */
    public AdRecordStore getAdRecordStore() {
        return mRecordStore;
    }

    /**
     * Gets the address.
     *
     * @return the address
     */
    public String getAddress() {
        return mDevice.getPeripheralDevice().getDeviceAddr();
    }

    /**
     * Gets the bluetooth device bond state.
     *
     * @return the bluetooth device bond state
     */
    public String getBluetoothDeviceBondState() {
        return resolveBondingState(mDevice.getAdvertiseFlag());
    }

    /**
     * Gets the device.
     *
     * @return the device
     */
    public BleScanResult getDevice() {
        return mDevice;
    }

    /**
     * Gets the first rssi.
     *
     * @return the first rssi
     */
    public int getFirstRssi() {
        return mFirstRssi;
    }

    /**
     * Gets the first timestamp.
     *
     * @return the first timestamp
     */
    public long getFirstTimestamp() {
        return mFirstTimestamp;
    }

    public String getName() {
        Optional<String> deviceName = mDevice.getPeripheralDevice().getDeviceName();
        if (deviceName.isPresent()) {
            return deviceName.get();
        }
        return "Unknown device";
    }

    /**
     * Gets the rssi.
     *
     * @return the rssi
     */
    public int getRssi() {
        return mCurrentRssi;
    }

    /**
     * Gets the rssi log.
     *
     * @return the rssi log
     */
    private Map<Long, Integer> getRssiLog() {
        synchronized (mRssiLog) {
            return mRssiLog;
        }
    }

    /**
     * Gets the running average rssi.
     *
     * @return the running average rssi
     */
    public double getRunningAverageRssi() {
        int sum = 0;
        int count = 0;
        synchronized (mRssiLog) {
            for (Map.Entry<Long, Integer> entry : mRssiLog.entrySet()) {
                count++;
                sum += mRssiLog.get(entry.getKey());
            }
        }
        if (count > 0) {
            return (float) sum / count;
        } else {
            return 0;
        }
    }

    /**
     * Gets the scan record.
     *
     * @return the scan record
     */
    public byte[] getScanRecord() {
        byte[] clone = mScanRecord.clone();
        return clone;
    }

    /**
     * Gets the timestamp.
     *
     * @return the timestamp
     */
    public long getTimestamp() {
        return mCurrentTimestamp;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + mCurrentRssi;
        result = prime * result + (int) (mCurrentTimestamp ^ (mCurrentTimestamp >>> 32));
        result = prime * result + ((mDevice == null) ? 0 : mDevice.hashCode());
        result = prime * result + mFirstRssi;
        result = prime * result + (int) (mFirstTimestamp ^ (mFirstTimestamp >>> 32));
        result = prime * result + ((mRecordStore == null) ? 0 : mRecordStore.hashCode());
        result = prime * result + ((mRssiLog == null) ? 0 : mRssiLog.hashCode());
        result = prime * result + Arrays.hashCode(mScanRecord);
        return result;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "BluetoothLeDevice [mDevice=" + mDevice + ", mRssi=" + mFirstRssi + ", mScanRecord=" + ByteUtils.byteArrayToHexString(mScanRecord) + ", mRecordStore=" + mRecordStore + ", getBleScanResult BondState()=" + ", getBleScanResult ClassName()=" + "]";
    }

    /**
     * Update rssi reading.
     *
     * @param timestamp   the timestamp
     * @param rssiReading the rssi reading
     */
    public void updateRssiReading(final long timestamp, final int rssiReading) {
        addToRssiLog(timestamp, rssiReading);
    }

    /**
     * Resolve bonding state.
     *
     * @param bondState the bond state
     * @return the string
     */
    private String resolveBondingState(int bondState) {
        switch (bondState) {
            default:
                return "Unknown";
        }
    }

    @Override
    public boolean marshalling(Parcel parcel) {
        parcel.writeSequenceable(mRecordStore);
        parcel.writeSequenceable(mDevice);
        parcel.writeMap(mRssiLog);
        parcel.writeByteArray(mScanRecord);
        parcel.writeInt(mFirstRssi);
        parcel.writeLong(mFirstTimestamp);
        parcel.writeInt(mCurrentRssi);
        parcel.writeLong(mCurrentTimestamp);
        return true;
    }

    @Override
    public boolean unmarshalling(Parcel parcel) {
        mRecordStore = (AdRecordStore) parcel.readValue();
        mDevice = (BleScanResult) parcel.readValue();
        mRssiLog = (Map<Long, Integer>) parcel.readMap();
        mScanRecord = parcel.readByteArray();
        mFirstRssi = parcel.readInt();
        mFirstTimestamp = parcel.readInt();
        mCurrentRssi = parcel.readInt();
        mCurrentTimestamp = parcel.readInt();
        return true;
    }
}